package com.example.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.example.domain.Shop;
import com.example.manager.BloomFilterManager;
import com.example.manager.RedisLimiterManager;
import com.example.manager.RedisLockManager;
import com.example.manager.ThreadPoolManager;
import com.example.mapper.ShopMapper;
import com.example.mq.MyMessageProducer;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import static com.example.common.RedisContants.*;

/**
 * 缓存问题工具类
 * 用于解决缓存击穿、雪崩、穿透问题，封装的工具类
 */
@Component
@Slf4j
public class RedisUtils {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private ShopMapper shopMapper;

    @Resource
    private RedisLockManager redisLockManager;

    @Resource
    private ThreadPoolManager threadPoolManager;

    @Resource
    private RedisLimiterManager redisLimiterManager;

    // 预期插入数量
    static long expectedInsertions = 200L;
    // 误判率
    static double falseProbability = 0.01;
    @Resource
    private BloomFilterManager bloomFilterManager;
    private RBloomFilter<Long> bloomFilter = null;

    @Resource
    private RedissonClient redissonClient;

    @Resource
    private MyMessageProducer myMessageProducer;


    /**
     * 缓存穿透——缓存无效Key、接口限流
     *
     * @param id
     * @return 1、现根据id查询Redis中数据是否存在
     * 如果数据存在——直接返回
     * 如果数据不存在
     * 去查询数据库，数据库中数据存在——直接返回并写入Redis中
     * 如果数据库中数据不存在，在Redis中缓存无效数据
     */
    public Shop queryWithPassThrough(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //TODO 分布式限流
        redisLimiterManager.doRateLimit(key);
        //1、从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断其字符串不是null，且不是空白字符。如果是真实存在的数据，那么它一定会卡在这一步
        if (StrUtil.isNotBlank(shopJson)) {
            //2.1、存在，直接返回——需要转成java对象返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            log.debug("Redis中获取到数据");
            return shop;
        }
        //判断命中的是否是空值
        if (shopJson != null) {
            //返回一个错误信息
            log.debug("Redis中key存在，但其数据不存在");
            return null;
        }

        //2.2、不存在，根据id查询数据库
        Shop shop = shopMapper.selectById(id);
        //3、数据库中不存在，返回错误
        if (shop == null) {
            //3.1、将空值写入缓存
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            log.debug("数据不存在，将其添加到Redis中，并返回空值");
            //3、返回
            return null;
        }
        //4、数据库中存在，写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //7、返回
        return shop;
    }


    /**
     * 缓存击穿——分布式锁
     * 在缓存穿透基础之上改造
     *
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1、从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断其字符串不是null，且不是空白字符。如果是真实存在的数据，那么它一定会卡在这一步
        if (StrUtil.isNotBlank(shopJson)) {
            //2.1、存在，直接返回——需要转成java对象返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            log.debug("Redis中获取到数据-2");
            return shop;
        }
        //判断命中的是否是空值
        if (shopJson != null) {
            //返回一个错误信息
            log.debug("Redis中key存在，但其数据不存在");
            return null;
        }
        //3、实现缓存重建
        //3.1、获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        Shop shop = null;
        try {
            boolean isLock = redisLockManager.tryLock(lockKey);
            //3.2、判断是否获取成功
            if (!isLock) {
                //3.3、失败，则休眠重试
                log.debug("获取锁失败，休眠重试!");
                Thread.sleep(50);
                //进行重试，再次执行
                Shop query = queryWithMutex(id);
                return query;
            }
            log.debug("获取锁成功，开始运行!");
            //根据id查询数据库
            shop = shopMapper.selectById(id);
            //模拟重建的延时——线程休眠200毫秒（0.2秒）
            Thread.sleep(200);
            //4、数据库中不存在，返回错误
            if (shop == null) {
                //4.1、将空值写入数据库
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                log.debug("数据不存在，将其添加到Redis中，并返回空值");
                //返回
                return null;
            }
            //5、数据库中存在，写入redis
            stringRedisTemplate.opsForValue()
                    .set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //6、释放互斥锁
            log.debug("释放互斥锁！");
            redisLockManager.unLock(lockKey);
        }
        //7、返回
        return shop;
    }

    /**
     * 缓存击穿——逻辑过期和互斥锁
     *
     * @param id
     * @return
     */
    public Shop queryWithLogicExpire(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //1、从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            log.debug("缓存中店铺数据为空！");
            return null;
        }
        //4、命中，需要把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //获取当前时间
        LocalDateTime localDateTime = LocalDateTime.now();
        //5、判断是否过期
        if (expireTime.isAfter(localDateTime)) {
            //5.1、未过期，直接返回店铺信息
            log.debug("时间未过期，返回店铺数据：{}", shop);
            return shop;
        }

        //5.2、已过期，需要缓存重建
        //6、缓存重建
        //6.1、获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = redisLockManager.tryLock(lockKey);
        //6.2、判断是否获取锁成功
        if (isLock) {
            log.debug("成功获取到锁，开始运行·····");
            //6.3、成功，开启独立线程，实现缓存重建
            threadPoolManager.submitTask(() -> {
                        try {
                            //重建缓存
                            this.saveShop2Redis(id, LOGIC_EXPIRE_TTL);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        } finally {
                            //释放锁
                            log.debug("操作结束，释放锁······");
                            redisLockManager.unLock(lockKey);
                        }
                    }
            );
        }
        //6.4、失败，返回过期的商铺信息
        return shop;
    }


    /**
     * 缓存重建逻辑
     * todo 代码中数据设置了逻辑过期时间，如果要是用到了缓存重建这段逻辑,代码中数据获取需要修改一下逻辑，要么就直接把缓存删除重新测试
     *
     * @param id
     * @param expireSecond
     */
    public void saveShop2Redis(Long id, Long expireSecond) {
        String key = CACHE_SHOP_KEY + id;
        //1、查询店铺数据
        Shop shop = shopMapper.selectById(id);
        //2、封装逻辑过期时间
        //RedisData redisData = new RedisData();
        //redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSecond));
        //redisData.setData(shop);
        //3、写入redis
        //stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));
    }


    // 项目启动的时候执行该方法，也可以理解为在spring容器初始化的时候执行该方法

    /**
     * TODO 1、初始化布隆过滤器，插入商品ID到布隆过滤器中,项目运行时开启。
     * 2、消息队列实现缓存预热
     */
    //@PostConstruct
    public void init() {
        //消息队列实现缓存预热
        //myMessageProducer.sendMessage("hmdp_exchange", "hmdp_routingKey", String.valueOf(1L));
        // 启动项目时初始化bloomFilter
        bloomFilter = bloomFilterManager.create(BLOOM_FILTER_SHOP, expectedInsertions, falseProbability);
        // 设置缓存时间
        int timeToLive = new Random().nextInt(200) + 300;
        // 设置bloomFilter的过期时间
        redissonClient.getBucket(BLOOM_FILTER_SHOP).set(null, timeToLive, TimeUnit.SECONDS);
        List<Long> list = shopMapper.selectAllIds();
        for (Long shopId : list) {
            bloomFilter.add(shopId);
        }
        long elementCount = bloomFilter.count();
        log.info("布隆过滤器加入元素个数为：{}.", elementCount);
    }

    /**
     * 缓存穿透——Redis布隆过滤器
     *
     * @param id
     * @return
     */
    public Shop bloomFilterPassThrough(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //String bloomKey = BLOOM_FILTER_SHOP + id;
        //判断一：bloomFilter中不存在该key,为非法访问
        if (!bloomFilter.contains(id)) {
            log.debug("所要查询的数据既不在缓存中，也不在数据库中，为非法key！");
            return null;
        }
        //1、从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断其字符串不是null，且不是空白字符。如果是真实存在的数据，那么它一定会卡在这一步
        if (StrUtil.isNotBlank(shopJson)) {
            //2.1、存在，直接返回——需要转成java对象返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            log.debug("数据在缓存中存在，正常返回！！！");
            return shop;
        }
        //2.2、不存在，根据id查询数据库
        Shop shop=shopMapper.selectById(id);
        //3、数据库中不存在，返回错误
        if (shop == null) {
            //todo 把“缓存空值”的代码逻辑删除
            log.debug("数据不存在，并返回空值");
            //3、返回
            return null;
        }
        //4、数据库中存在，写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //7、返回
        return shop;
    }

    public Integer InsertData(Shop shop) {
        //1、插入数据库
        int insert = shopMapper.insert(shop);
        //2、插入布隆过滤器
        bloomFilter.add(shop.getId());
        //3、返回
        return insert;
    }
}
